"""Minerva core objects.

To create a visible resource in the system the following actions are
taken on behalf of a User (Author):

1) A new Content instance is created and customized with attributes
provided through ContentType handler.
2) A new Noun instance is created for the specified URL path and is
linked to the Content instance. At this point only the Author will
be able to see the new piece of Content at path. 

To share the new piece of Content with other users (the audience)
a SHARE NounEvent must be created with audience usernames specified.

Multiple Nouns can reference a single Content instance.

An Author can add other Users as Coauthors to a Content instance she owns.
Coauthors can modify that Content and create new Nouns that point to it.
"""

import itertools
import time
import yaml as pyyaml

from google.appengine.ext import db

import render

SLASH = '/'
GLOB = '*'
SHOW = 'show'
REMOVE = 'remove'
XML_STANZA = '<?xml version="1.0" encoding="UTF-8"?>'


def DatetimeToSeconds(dt):
  """Converts a datetime.datetime to seconds since Unix epoch.

  @arg dt: datetime.datetime
  @return: float
  """
  return time.mktime(dt.timetuple())+1e-6*dt.microsecond


class _ContentTypeRegistry(object):
  """Simple registry for ContentType handlers.
  """

  def __init__(self, initdata=None):
    """Initialize.

    @arg initdata: dict; Pre-existing map to use in lieu of an empty new one.
    """
    self._registry = initdata or {}
    self._order = []

  def Configure(self, config_path):
    """Extend registry by reading in YAML file at config_path.

    Entries take the following form:

      ---
      name: webpage
      desc: A web page at some url

    In the above case a module called an attempt is made to
    load a module called webpage from the content_types package.
    If found, an attempt is made to retrieve a class called Plugin.
    If found, it is registered.

    @arg config_path: str; Path to YAML content type registry config file.
    """
    fi = open(config_path)
    config = pyyaml.load_all(fi)
    for entry in config:
      name = entry.get('name')
      desc = entry.get('desc')

      # Load in plugin
      try:
        mod = __import__('content_types.%s' % name, globals(), {}, [1])
      except (ImportError, NameError), e:
        continue

      plugin = getattr(mod, 'Plugin', None)
      plugin.name = name
      plugin.desc = desc

      self._registry[name] = plugin
      self._order.append(name)

  def Get(self, content_type_name):
    """Retrieve transformer to convert content_type to response_format.

    @arg content_type_name: str; Retrieve a registered ContentType by name.
    @return: ContentType implementor;
    """
    return self._registry.get(content_type_name)

  def __iter__(self):
    reg = self._registry
    return (reg[name] for name in self._order)


registry = _ContentTypeRegistry()


class Content(db.Expando):
  """Datastructure for some type of Content created within the system.

  Since this is an Expando, users will need to add properties that
  contain the actual content source for the type.
  """
  author = db.UserProperty(
      verbose_name='User who created this content instance',
      required=True)
  title = db.StringProperty(
      verbose_name='Unique identifier within author/type space')
  type = db.StringProperty(
      verbose_name='Content type identifier', required=True)
      
  subtype = db.StringProperty()
  revision = db.IntegerProperty()

  created = db.DateTimeProperty(auto_now_add=True)
  updated = db.DateTimeProperty(auto_now=True)
  published = db.DateTimeProperty(default=None)

  src = db.TextProperty()
  url = db.StringProperty()

  def AddCoauthor(self, user):
    """Add a Coauthor to this Content instance.

    @arg user: User to add as Coauthor.
    @return: Coauthor
    """
    existing = db.GqlQuery(
        'SELECT * FROM Coauthor WHERE content = :1 and user = :2',
        self, user)

    if existing.count():
      return

    ca = Coauthor(self, user=user, content=self)
    ca.put()
    return ca

  def Html(self, editor=False, **kwargs):
    """Retrieve HTML representation of this Content.

    @arg editor: bool; Editor mode.
    @arg kwargs: dict; To pass to handler.
    @return: str; HTML
    """
    handler = registry.Get(self.type)
    if handler:
      return handler.Html(self, editor=editor, **kwargs)

  def Json(self, **kwargs):
    """Retrieve JSON representation of this Content.

    @arg kwargs: dict; To pass to handler.
    @return: str; JSON
    """
    handler = registry.Get(self.type)
    if handler:
      return handler.Json(self)

  def Xml(self, **kwargs):
    """Retrieve XML representation of this Content.

    @arg kwargs: dict; To pass to handler.
    @return: str; XML
    """
    handler = registry.Get(self.type)
    if handler:
      return handler.Xml(self)


class Annotation(db.Expando):
  """A note, metadata or list of tags that can be placed on a Content instance.

  Since this is an Expando, users will need to add properties that
  contain the actual content source for the annotation.
  There is a simple Text field provided, however.
  """
  author = db.UserProperty(
      verbose_name='User who created this annotation instance',
      required=True
      )
  content = db.Reference(Content, required=True)
  created = db.DateTimeProperty(auto_now_add=True)
  text = db.TextProperty()
  tags = db.StringListProperty()


class Coauthor(db.Model):
  """Coauthor for a Content instance.
  """
  user = db.UserProperty(required=True)
  content = db.Reference(Content, required=True)


# TODO: Move to sharded counter
class PseudoWorldUniqueNumberGenerator(db.Model):
  """Trying (perhaps in vain) to generate world unique numbers under datastore

  Essentially a singleton. This is a temporary hack.
  """
  counter = db.IntegerProperty(default=0)

  @classmethod
  def Next(cls, benefactor=None):
    """Increment counter.

    @return: integer
    """
    if benefactor:
      kind = benefactor.kind()
    else:
      kind = 'default'

    instance = cls.get_or_insert(kind)
    instance.counter += 1
    instance.put()
    return instance.counter


class Noun(db.Model):
  """A node in the URL Noun Space.

  At creation I will only be viewable by the user that created me.
  To allow others to view the content here create NounEvents
  connected to me (Using the convenience methods).
  """
  creator = db.UserProperty(required=True)
  path = db.StringProperty(required=True)
  content = db.Reference(Content, required=True)
  created = db.DateTimeProperty(auto_now_add=True)

  def ShowTo(self, *usernames):
    """Allow specified users to view content at this location.

    If no usernames are specified this is construed as allowing everyone.

    @arg usernames: list; Usernames to allow.
    """
    # Derive the pwun
    pwun = PseudoWorldUniqueNumberGenerator.Next(self)
    ev = NounEvent(
        pwun=pwun, creator=self.creator, noun=self,
        noun_path=self.path, action=SHOW, audience=list(usernames))
    ev.put()

  def RemoveFrom(self, *usernames):
    """Remove users ability to view content at this location.

    If no usernames are specified this is construed as removing everyone.

    @arg usernames: list; Usernames to remove.
    """
    # Derive the pwun
    pwun = PseudoWorldUniqueNumberGenerator.Next(self)
    ev = NounEvent(
        pwun=pwun, creator=self.creator, noun=self,
        noun_path=self.path, action=REMOVE, audience=usernames)
    ev.put()

  @classmethod
  def New(cls, creator, path, content):
    """Create a new Noun instance

    @arg creator: User this instance is being created for.
    @arg path: str; URL this Noun represents.
    @arg content: Content
    @return: Noun
    """
    if path != SLASH and path[-1] == SLASH:
      path = path[:-1]

    noun = cls(creator=creator, path=path, content=content)
    noun.put()
    return noun

  @classmethod
  def Find(cls, path):
    """Find Noun at path.

    @arg path: str; URL path to find a Noun for.
    @return: Noun
    """
    if path != SLASH and path[-1] == SLASH:
      path = path[:-1]

    query = cls.gql('WHERE path = :1 LIMIT 1', path)
    return query.get()

  @classmethod
  def Destroy(cls, instance):
    """Destroy a Noun instance and create an event to indicate that.

    @arg instance: Noun to destroy.
    """
    # Derive the pwun
    pwun = PseudoWorldUniqueNumberGenerator.Next(instance)
    ev = NounEvent(
        pwun=pwun, creator=self.creator, noun=instance,
        noun_path=noun.path, action=REMOVE)
    ev.put()
    instance.delete()


class NounEvent(db.Model):
  """An event in the Noun space.

  These are used to communicate changes in the NounSpace.
  For example, to keep Cloud store and Gears store in sync.
  """
  pwun = db.IntegerProperty(required=True) # Pseudo World Unique Number
  noun = db.Reference(Noun, required=True)

  creator = db.UserProperty(required=True)
  noun_path = db.StringProperty(required=True)
  audience = db.StringListProperty()

  action = db.StringProperty(required=True, choices=(SHOW, REMOVE))
  timestamp = db.DateTimeProperty(auto_now_add=True)

  @property
  def meta(self):
    """Metadata that will be seen by clients during sync.

    @return: list;
        [pwun, path, action, username, timestamp,
        content_type_name, content_title]
    """
    noun = self.noun 
    if noun:
      content_type = noun.content.type
      content_title = noun.content.title
    else:
      # Our noun was deleted
      content_type, content_title = None, None

    return [
        self.pwun,
        self.noun_path,
        self.action,
        str(self.creator),
        DatetimeToSeconds(self.timestamp),
        content_type,
        content_title]

  @classmethod
  def Since(cls, last_pwun, to, limit=50):
    """Retrieve NounEvents created since last_pwun shared to username.

    @arg last_pwun: integer;
    @arg to: User;
    @return: list; [NounEvent, ...]
    """
    if last_pwun is None:
      last_pwun = 1
    else:
      try:
        last_pwun = int(last_pwun) + 1
      except ValueError:
        last_pwun = 1

    return cls.gql(
      'WHERE pwun >= :1 AND audience in :2 ORDER BY pwun ASC LIMIT %d' % limit,
      last_pwun, [to, GLOB])


class ContentType(object):
  """Interface and base implementation for ContentTypes.
  """
  name = None
  desc = None

  @classmethod
  def Validate(cls, **params):
    """Validate params submitted by client.

    Users must define this in subclasses.
    """
    raise NotImplementedError

  @classmethod
  def New(cls, author, title, **kwargs):
    """Create a new Content instance with its .type set to cls.name.

    @arg author: User we are creating Content on behalf of.
    @arg title: Title of Content instance.
    @arg kwargs: dict; Attributes to use to .Customize() Content instance.

    @return: Content 
    """
    # cls.Validate(**kwargs)
    content = Content(author=author, title=title, type=cls.name)
    cls.Customize(content, **kwargs)
    content.put()
    return content

  @classmethod
  def Customize(cls, content, **kwargs):
    """Customize new Content instance.

    Users must define this in subclasses.

    @arg content: Content to customize.
    @arg kwargs: dict; Attributes used in customization.
    """
    raise NotImplementedError

  @classmethod
  def Find(cls, user, title):
    """Find a Content instance by author/title.

    @arg user: User;
    @arg title: str;
    @return: Content
    """
    return Content.gql(
        'WHERE type = :1 and author = :2 and title = :3',
        cls.name, user, title)

  @classmethod
  def Html(cls, content, editor=False, **kwargs):
    """Default HTML renderer for Content instances.

    Passes content and kwargs into render.Render along
    with corresponding template for ContentType plugin.

    @arg content: Content instance to generate HTML representation of.
    @arg editor: bool; Editor mode. 
    @arg kwargs: dict;
    @returns: str; HTML
    """
    template_dir = 'content_types'
    template_name = cls.name + '.html'
    return render.Render(
        template_dir, template_name, content_type=cls, content=content,
        editor=editor, **kwargs)

  @classmethod
  def Json(cls, content, **kwargs):
    """No default implementation yet
    """

  @classmethod
  def Xml(cls, content, **kwargs):
    """This is easy. Every datastore model has a to_xml method.
    """
    return '%s\n%s' % (XML_STANZA, content.to_xml())


class UrlContentType(ContentType):
  """
  """

  @classmethod
  def Customize(cls, content, url):
    content.url = url


class XmlContentType(ContentType):
  """
  """

  @classmethod
  def Customize(cls, content, xml, subtype=None):
    content.src = xml
    content.subtype = subtype
